package io.gitee.zhangbinhub.acp.cloud.resource.server.component

import io.gitee.zhangbinhub.acp.cloud.tools.CloudTools
import io.gitee.zhangbinhub.acp.cloud.resource.server.tools.TokenTools
import org.springframework.core.ParameterizedTypeReference
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.core.io.buffer.DataBufferUtils
import org.springframework.http.*
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames
import org.springframework.security.oauth2.server.resource.introspection.*
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientException
import reactor.core.publisher.Mono

class AcpOpaqueTokenReactiveIntrospect(
    private val cloudTools: CloudTools,
    private val introspectionUri: String,
    private val webClientBuilder: WebClient.Builder
) : ReactiveOpaqueTokenIntrospector {
    private val stringObjectMap: ParameterizedTypeReference<MutableMap<String, Any?>> =
        object : ParameterizedTypeReference<MutableMap<String, Any?>>() {}

    @Throws(OAuth2IntrospectionException::class)
    override fun introspect(token: String): Mono<OAuth2AuthenticatedPrincipal> = Mono.just(token)
        .flatMap { makeRequest(it) }
        .map { TokenTools.convertClaimsSet(it) }
        .onErrorMap({ e -> e is OAuth2IntrospectionException }, { e -> OAuth2IntrospectionException(e.message, e) })

    @Throws(OAuth2IntrospectionException::class, BadOpaqueTokenException::class)
    private fun makeRequest(token: String): Mono<MutableMap<String, Any?>> =
        try {
            webClientBuilder.filter(cloudTools.headerFilterFunction(false)).build().post()
                .uri(this.introspectionUri)
                .accept(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromFormData("token", token))
                .exchangeToMono { response ->
                    when (response.statusCode()) {
                        HttpStatus.OK -> {
                            response.bodyToMono(stringObjectMap).filter { body ->
                                (body?.compute(OAuth2TokenIntrospectionClaimNames.ACTIVE) { _: String, value: Any? ->
                                    if (value is String) {
                                        return@compute value.toBoolean()
                                    }
                                    if (value is Boolean) {
                                        return@compute value
                                    }
                                    false
                                } ?: false) as Boolean
                            }.switchIfEmpty(Mono.error(BadOpaqueTokenException("Provided token isn't active")))
                        }

                        HttpStatus.UNAUTHORIZED -> {
                            response.bodyToFlux(DataBuffer::class.java).map { DataBufferUtils.release(it) }
                                .then(Mono.error(BadOpaqueTokenException("invalid_client")))
                        }

                        else -> {
                            response.bodyToFlux(DataBuffer::class.java).map { DataBufferUtils.release(it) }
                                .then(Mono.error(OAuth2IntrospectionException("Introspection endpoint responded with ${response.statusCode()}")))
                        }
                    }
                }
        } catch (ex: WebClientException) {
            throw BadOpaqueTokenException(ex.message, ex)
        } catch (ex: Exception) {
            throw OAuth2IntrospectionException(ex.message, ex)
        }
}